import numpy


def sigmoid(x):
    return 1.0 / (1.0 + numpy.exp(-x))

def tanh(x):
    return numpy.tanh(x)

def relu(x):
    return numpy.maximum(0, x)

def leaky_relu(x, alpha=0.01):
    return numpy.where(x > 0, x, alpha * x)

def prelu(x, alpha):
    return numpy.where(x > 0, x, alpha * x)


class Layer:
    def __init__(self, units, activation):
        self.units = units
        self.activation = activation

    def __call__(self, weights, x):
        return self.activation(numpy.dot(weights, x))


class LinearNN:
    def __init__(self, *layers):
        self.layers = layers

        self.weights_group = []
        for ip_layer, op_layer in zip(self.layers[:-1], self.layers[1:]):
            self.weights_group.append(
                numpy.random.normal(0.0, pow(op_layer.units, -0.5), (op_layer.units, ip_layer.units))
            )

        self.lr = None
        self.d_min = None
        self.d_max = None
        self.k = None

    def compile(self, lr, d_min, d_max):
        self.lr = lr
        self.d_min = d_min
        self.d_max = d_max
        if self.d_min is not None and self.d_max is not None:
            self.k = (1.0 - 0.0) / (self.d_max - self.d_min)

    def _offset(self, x):
        return ((0.0 + self.k * (x - self.d_min)) * 0.99) + 0.01

    def train(self, x, y):
        if self.k is not None:
            x = self._offset(numpy.array(x, ndmin=2).T)
        else:
            x = numpy.array(x, ndmin=2).T

        x_group = [x]
        for layer, weights in zip(self.layers[1:], self.weights_group):
            x_group.append(layer(weights, x_group[-1]))

        y = numpy.array([(0.01 if v != y else 0.99) for v in range(0, self.layers[-1].units)], ndmin=2).T

        error_group = [y - x_group[-1]]
        for weights in list(reversed(self.weights_group))[:-1]:
            error_group.append(numpy.dot(weights.T, error_group[-1]))

        for weights, error, curr_x, prev_x in zip(reversed(self.weights_group), error_group, list(reversed(x_group))[:-1], list(reversed(x_group))[1:]):
            weights += self.lr * numpy.dot((error * curr_x * (1.0 - curr_x)), numpy.transpose(prev_x))

    def query(self, x):
        if self.k is not None:
            x = self._offset(numpy.array(x, ndmin=2).T)
        else:
            x = numpy.array(x, ndmin=2).T

        for layer, weights in zip(self.layers[1:], self.weights_group):
            x = layer(weights, x)

        return numpy.argmax(x)

    def save(self, path=".\\lnn_model"):
        model = [weight.tolist() for weight in self.weights_group]

        with open(path, "w", encoding="utf-8") as f_obj:
            f_obj.write(str(model))

    def load(self, path=".\\lnn_model"):
        with open(path, "r", encoding="utf-8") as f_obj:
            model = eval(f_obj.read())

        self.weights_group = [numpy.array(weight) for weight in model]